Skip to content

🛡️ Sentinel: Fix TOCTOU race condition in SSH key creation#32

Open
kidchenko wants to merge 1 commit intomainfrom
sentinel/fix-ssh-key-race-condition-4005203712850023242
Open

🛡️ Sentinel: Fix TOCTOU race condition in SSH key creation#32
kidchenko wants to merge 1 commit intomainfrom
sentinel/fix-ssh-key-race-condition-4005203712850023242

Conversation

@kidchenko
Copy link
Owner

@kidchenko kidchenko commented Feb 16, 2026

🛡️ Sentinel: [CRITICAL] Fix TOCTOU race condition in SSH key creation

🚨 Severity: CRITICAL
💡 Vulnerability: Private SSH keys were created with default umask permissions (often 644) before chmod 600 was applied, creating a race window where keys were world-readable.
🎯 Impact: Local attackers could potentially read private SSH keys during the brief window between creation and permission restriction.
🔧 Fix: Used (umask 077; op read ... > file) to ensure atomic creation with 600 permissions.
✅ Verification: Added tests/security_ssh_permissions.sh to verify that umask affects file creation permissions and that existing files retain permissions (justifying the removal step).


PR created automatically by Jules for task 4005203712850023242 started by @kidchenko

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed SSH key creation race condition that could temporarily expose keys with insecure permissions.
  • Documentation

    • Added documentation about file permission vulnerabilities in shell scripts.
  • Tests

    • Added tests to verify secure SSH key file creation and permissions.

- Use `(umask 077; ...)` subshell to ensure private keys are created with 600 permissions atomically.
- Remove existing file before creation to ensure permissions are reset.
- Add regression test `tests/security_ssh_permissions.sh`.
- Update Sentinel journal.

Co-authored-by: kidchenko <5432753+kidchenko@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

This PR addresses a TOCTOU race condition in SSH key creation by introducing a restrictive umask pattern. It documents the vulnerability, adds test coverage to verify secure file permissions, and implements the fix in the key setup script to prevent private keys from being briefly world-readable.

Changes

Cohort / File(s) Summary
Documentation
.jules/sentinel.md
Documents the TOCTOU race condition vulnerability in shell scripts where private SSH keys become world-readable before permissions are restricted. Captures the prevention pattern using subshell with restrictive umask.
Tests
tests/security_ssh_permissions.sh
New test script with cross-platform permission checking that verifies insecure default behavior, secure umask 077 creation, and behavior when overwriting existing files with different permissions.
Implementation
tools/setup-ssh-keys.sh
Fixes private key creation by wrapping the read operation in a subshell with umask 077 and removing existing files before recreation. Ensures private key files have 600 permissions without a readable window.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A race condition was fixed with care,
SSH keys locked tight, not laid bare,
With umask seventy-seven so tight,
The burrow's secrets stay safe and right. 🔐

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing a TOCTOU race condition in SSH key creation, which is the primary security improvement across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sentinel/fix-ssh-key-race-condition-4005203712850023242

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.jules/sentinel.md:
- Around line 1-4: Update the markdown so the first line is a top-level heading
and the date is corrected: replace the existing "## 2025-02-16 - TOCTOU Race
Condition in Shell Scripts" with "# 2026-02-16 - TOCTOU Race Condition in Shell
Scripts"; add a single blank line immediately after that heading; then wrap or
reflow the following paragraph lines (the vulnerability, learning, and
prevention sentences) to be <=80 characters per line (preserving content such as
`(umask 077; command > file)` and `chmod 600`) and ensure any suggested code
snippets remain inline or fenced appropriately so MD013 and MD022/MD041 lint
rules pass.

In `@tests/security_ssh_permissions.sh`:
- Around line 53-60: The else branch prints a stale $perms from Test 1;
recompute the current permission before using it: after the failed
check_permissions "$TEST_FILE" "644" case, refresh perms for "$TEST_FILE" (e.g.,
via stat) and then use that updated $perms in the echo, or change the echo to
report the result of a permission-query helper instead of the old $perms; update
references to the variable name perms and the TEST_FILE usage so the message
reflects current permissions.
🧹 Nitpick comments (3)
tools/setup-ssh-keys.sh (1)

164-167: Consider applying the same atomic pattern for the public key write.

While the public key is not sensitive (ends up 644), the current pattern on line 166 still has the same write-then-chmod ordering. If consistency matters to you, you could wrap it similarly — but this is purely a style/consistency nit since 644 is the intended final permission anyway.

tests/security_ssh_permissions.sh (2)

27-39: Use the check_permissions helper instead of duplicating stat logic.

Line 34 reimplements the cross-platform stat logic with a try/fallback approach, while the check_permissions helper (lines 8–25) already handles this correctly via uname. For consistency and to avoid divergence, consider reusing the helper here.

Proposed fix
 echo "=== Test 1: Insecure Creation (Baseline) ==="
 rm -f "$TEST_FILE"
 # Standard creation (vulnerable pattern)
 echo "secret" > "$TEST_FILE"
-# Check if permissions are 644 (or 664 depending on umask)
-# Assuming default umask 022 -> 644
-# We check if it is NOT 600
-perms=$(stat -c "%a" "$TEST_FILE" 2>/dev/null || stat -f "%Lp" "$TEST_FILE")
-if [[ "$perms" != "600" ]]; then
-    echo "PASS: Default creation is insecure ($perms)"
+# Verify default creation does NOT produce 600
+if ! check_permissions "$TEST_FILE" "600" 2>/dev/null; then
+    echo "PASS: Default creation is insecure (not 600)"
 else
     echo "WARN: Default umask is already strict (077)? This test assumes default umask allows group/other read."
 fi

5-5: Test file path is relative — fragile if run from a non-root directory.

TEST_FILE="tests/ssh_key_test" assumes the working directory is the repo root. Consider using a temp file or deriving the path from $0.

Example
-TEST_FILE="tests/ssh_key_test"
+TEST_DIR="$(cd "$(dirname "$0")" && pwd)"
+TEST_FILE="$TEST_DIR/ssh_key_test"

Comment on lines +1 to +4
## 2025-02-16 - TOCTOU Race Condition in Shell Scripts
**Vulnerability:** Private SSH keys were created using redirection (`> file`) before restricting permissions (`chmod 600`), leaving a race window where keys were world-readable.
**Learning:** Shell redirection creates files with default umask permissions (often 644) *before* any subsequent `chmod` command runs.
**Prevention:** Use `(umask 077; command > file)` in a subshell to ensure files are created with restrictive permissions atomically. Also ensure existing files are removed before recreation.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix markdown lint failures flagged by CI.

The documentation lint check is failing with multiple issues:

  1. MD041: First line should be a top-level heading (#), not ##.
  2. MD022: A blank line is needed after the heading.
  3. MD013: Lines 2–4 exceed the 80-character line length limit.

Also, the date reads 2025-02-16 — should this be 2026-02-16?

Proposed fix
-## 2025-02-16 - TOCTOU Race Condition in Shell Scripts
-**Vulnerability:** Private SSH keys were created using redirection (`> file`) before restricting permissions (`chmod 600`), leaving a race window where keys were world-readable.
-**Learning:** Shell redirection creates files with default umask permissions (often 644) *before* any subsequent `chmod` command runs.
-**Prevention:** Use `(umask 077; command > file)` in a subshell to ensure files are created with restrictive permissions atomically. Also ensure existing files are removed before recreation.
+# Sentinel Journal
+
+## 2026-02-16 - TOCTOU Race Condition in Shell Scripts
+
+**Vulnerability:** Private SSH keys were created using
+redirection (`> file`) before restricting permissions
+(`chmod 600`), leaving a race window where keys were
+world-readable.
+
+**Learning:** Shell redirection creates files with default
+umask permissions (often 644) *before* any subsequent
+`chmod` command runs.
+
+**Prevention:** Use `(umask 077; command > file)` in a
+subshell to ensure files are created with restrictive
+permissions atomically. Also ensure existing files are
+removed before recreation.
🧰 Tools
🪛 GitHub Check: Lint Documentation

[failure] 4-4: Line length
.jules/sentinel.md:4:81 MD013/line-length Line length [Expected: 80; Actual: 190] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 3-3: Line length
.jules/sentinel.md:3:81 MD013/line-length Line length [Expected: 80; Actual: 134] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 2-2: Line length
.jules/sentinel.md:2:81 MD013/line-length Line length [Expected: 80; Actual: 177] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 1-1: First line in a file should be a top-level heading
.jules/sentinel.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## 2025-02-16 - TOCTOU Race Co..."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md


[failure] 1-1: Headings should be surrounded by blank lines
.jules/sentinel.md:1 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## 2025-02-16 - TOCTOU Race Condition in Shell Scripts"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md

🤖 Prompt for AI Agents
In @.jules/sentinel.md around lines 1 - 4, Update the markdown so the first line
is a top-level heading and the date is corrected: replace the existing "##
2025-02-16 - TOCTOU Race Condition in Shell Scripts" with "# 2026-02-16 - TOCTOU
Race Condition in Shell Scripts"; add a single blank line immediately after that
heading; then wrap or reflow the following paragraph lines (the vulnerability,
learning, and prevention sentences) to be <=80 characters per line (preserving
content such as `(umask 077; command > file)` and `chmod 600`) and ensure any
suggested code snippets remain inline or fenced appropriately so MD013 and
MD022/MD041 lint rules pass.

Comment on lines +53 to +60
# Try to overwrite with umask 077 (simulating the fix applied blindly)
(umask 077; echo "new content" > "$TEST_FILE")
# Should still be 644 because file existed
if check_permissions "$TEST_FILE" "644"; then
echo "Confirmed: Overwriting existing file preserves insecure permissions (Must delete file first!)"
else
echo "Unexpected: Permissions changed to $perms?"
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bug: $perms on line 59 is stale from Test 1.

The $perms variable was set on line 34 during Test 1 and is never updated in Test 3. The else branch on line 59 will print the old value, not the file's current permissions.

Proposed fix
 # Try to overwrite with umask 077 (simulating the fix applied blindly)
 (umask 077; echo "new content" > "$TEST_FILE")
 # Should still be 644 because file existed
 if check_permissions "$TEST_FILE" "644"; then
     echo "Confirmed: Overwriting existing file preserves insecure permissions (Must delete file first!)"
 else
-    echo "Unexpected: Permissions changed to $perms?"
+    perms_actual=$(if [[ "$(uname -s)" == "Darwin" ]]; then stat -f "%Lp" "$TEST_FILE"; else stat -c "%a" "$TEST_FILE"; fi)
+    echo "Unexpected: Permissions changed to $perms_actual?"
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Try to overwrite with umask 077 (simulating the fix applied blindly)
(umask 077; echo "new content" > "$TEST_FILE")
# Should still be 644 because file existed
if check_permissions "$TEST_FILE" "644"; then
echo "Confirmed: Overwriting existing file preserves insecure permissions (Must delete file first!)"
else
echo "Unexpected: Permissions changed to $perms?"
fi
# Try to overwrite with umask 077 (simulating the fix applied blindly)
(umask 077; echo "new content" > "$TEST_FILE")
# Should still be 644 because file existed
if check_permissions "$TEST_FILE" "644"; then
echo "Confirmed: Overwriting existing file preserves insecure permissions (Must delete file first!)"
else
perms_actual=$(if [[ "$(uname -s)" == "Darwin" ]]; then stat -f "%Lp" "$TEST_FILE"; else stat -c "%a" "$TEST_FILE"; fi)
echo "Unexpected: Permissions changed to $perms_actual?"
fi
🤖 Prompt for AI Agents
In `@tests/security_ssh_permissions.sh` around lines 53 - 60, The else branch
prints a stale $perms from Test 1; recompute the current permission before using
it: after the failed check_permissions "$TEST_FILE" "644" case, refresh perms
for "$TEST_FILE" (e.g., via stat) and then use that updated $perms in the echo,
or change the echo to report the result of a permission-query helper instead of
the old $perms; update references to the variable name perms and the TEST_FILE
usage so the message reflects current permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant